/**
 * Copyright(c) 2013 Jade Techonologies Co., Ltd.
 *
 * History:
 *   13-4-26 上午9:51 Created by dwzhang
 */
package com.jade.framework.base.security.auth.login;

import java.io.IOException;
import java.util.Map;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;

import com.jade.framework.base.security.auth.callback.ParametersCallback;
import com.jade.framework.base.util.Parameters;

/**
 * 一个抽象的使用Parameters传递参数的登录模块。
 *
 * @author dwzhang
 * @version 1.0 13-4-26 上午9:51
 */
public abstract class AbstractLoginModule implements LoginModule
{
    /** <p>The authentication status.</p> */
    protected boolean success;

    /** <p>The commit status.</p> */
    protected boolean commitSuccess;

    /** <p>The Subject to be authenticated.</p> */
    protected Subject subject;

    /** <p>A CallbackHandler for communicating with the end user (prompting for usernames and passwords, for example).</p> */
    protected CallbackHandler callbackHandler;

    /** <p>State shared with other configured LoginModules.</p> */
    protected Map sharedState;

    /** <p>Options specified in the login Configuration for this particular LoginModule.</p> */
    protected Map options;

    /** <p>The user credential info.</p> */
    protected Parameters params;

    /** <p>The default login module constructor.</p> */
    public AbstractLoginModule()
    {
        success = false;
        commitSuccess = false;
        params = null;
    }

    /**
     * Initialize this LoginModule.
     * <p/>
     * <p> This method is called by the <code>LoginContext</code>
     * after this <code>LoginModule</code> has been instantiated.
     * The purpose of this method is to initialize this
     * <code>LoginModule</code> with the relevant information.
     * If this <code>LoginModule</code> does not understand
     * any of the data stored in <code>sharedState</code> or
     * <code>options</code> parameters, they can be ignored.
     * <p/>
     * <p/>
     *
     * @param subject         the <code>Subject</code> to be authenticated. <p>
     * @param callbackHandler a <code>CallbackHandler</code> for communicating
     *                        with the end user (prompting for usernames and
     *                        passwords, for example). <p>
     * @param sharedState     state shared with other configured LoginModules. <p>
     * @param options         options specified in the login
     *                        <code>Configuration</code> for this particular
     *                        <code>LoginModule</code>.
     */
    public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState,
            Map<String, ?> options)
    {
        this.subject = subject;
        this.callbackHandler = callbackHandler;
        this.sharedState = sharedState;
        this.options = options;
    }

    /**
     * 内容为用户凭证信息，抽象的方法定义了需要扩展该方法来完成认证的操作。
     *
     * @param subject 主题
     * @param context 参数
     * @return true　表示验证成功；　false 表示验证失败
     * @throws javax.security.auth.login.LoginException
     *          异常
     */
    protected abstract boolean doLogin(Subject subject, Parameters context)
            throws LoginException;

    /**
     * Method to authenticate a <code>Subject</code> (phase 1).
     * <p/>
     * <p> The implementation of this method authenticates
     * a <code>Subject</code>.  For example, it may prompt for
     * <code>Subject</code> information such
     * as a username and password and then attempt to verify the password.
     * This method saves the result of the authentication attempt
     * as private state within the LoginModule.
     * <p/>
     * <p/>
     *
     * @return true if the authentication succeeded, or false if this
     *         <code>LoginModule</code> should be ignored.
     * @throws javax.security.auth.login.LoginException
     *          if the authentication fails
     */
    public boolean login()
            throws LoginException
    {
        if (callbackHandler == null) {
            throw new LoginException(
                    "Error: no CallbackHandler available " + "to garner authentication information from the user");
        }
        try {
            params = getParameters(callbackHandler);
            if (params == null) {
                throw new FailedLoginException("Can't get login parameters!");
            }

            success = doLogin(subject, params);

            if (!success) {
                throw new FailedLoginException("Authentication failed: user credential info does not match");
            }
            return true;
        }
        catch (LoginException ex) {
            success = false;
            throw ex;
        }
        catch (Exception ex) {
            success = false;
            throw new LoginException(ex.getMessage());
        }
    }

    protected Parameters getParameters(CallbackHandler callbackHandler)
            throws IOException, UnsupportedCallbackException
    {
        // Setup default callback handlers.
        Callback[] callbacks = new Callback[]{new ParametersCallback()};
        callbackHandler.handle(callbacks);
        if (callbacks[0] instanceof ParametersCallback) {
            return ((ParametersCallback)callbacks[0]).getParameters();
        }
        else {
            return null;
        }
    }

    /**
     * Method to commit the authentication process (phase 2).
     * <p/>
     * <p> This method is called if the LoginContext's
     * overall authentication succeeded
     * (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules
     * succeeded).
     * <p/>
     * <p> If this LoginModule's own authentication attempt
     * succeeded (checked by retrieving the private state saved by the
     * <code>login</code> method), then this method associates relevant
     * Principals and Credentials with the <code>Subject</code> located in the
     * <code>LoginModule</code>.  If this LoginModule's own
     * authentication attempted failed, then this method removes/destroys
     * any state that was originally saved.
     * <p/>
     * <p/>
     *
     * @return true if this method succeeded, or false if this
     *         <code>LoginModule</code> should be ignored.
     * @throws javax.security.auth.login.LoginException
     *          if the commit fails
     */
    public boolean commit()
            throws LoginException
    {
        if (success) {
            if (subject.isReadOnly()) {
                throw new LoginException("Subject is Readonly");
            }
            try {
                // Collection<Principal> principals = this.getPrincipals(params);
                // subject.getPrincipals().addAll(principals);
                doCommit(subject, params);
                commitSuccess = true;

                params = null;
                commitSuccess = true;
            }
            catch (Exception ex) {
                throw new LoginException(ex.getMessage());
            }
        }

        return commitSuccess;
    }

    /**
     * 执行COMMIT的操作.
     *
     * @param subject 主题
     * @param context 参数
     * @throws javax.security.auth.login.LoginException
     *          异常
     */
    protected abstract void doCommit(Subject subject, Parameters context)
            throws LoginException;

    /**
     * Method to abort the authentication process (phase 2).
     * <p/>
     * <p> This method is called if the LoginContext's
     * overall authentication failed.
     * (the relevant REQUIRED, REQUISITE, SUFFICIENT and OPTIONAL LoginModules
     * did not succeed).
     * <p/>
     * <p> If this LoginModule's own authentication attempt
     * succeeded (checked by retrieving the private state saved by the
     * <code>login</code> method), then this method cleans up any state
     * that was originally saved.
     * <p/>
     * <p/>
     *
     * @return true if this method succeeded, or false if this
     *         <code>LoginModule</code> should be ignored.
     * @throws javax.security.auth.login.LoginException
     *          if the abort fails
     */
    public boolean abort()
            throws LoginException
    {
        // Clean out state
        success = false;
        commitSuccess = false;
        params = null;
        logout();
        return true;
    }

    /**
     * Method which logs out a <code>Subject</code>.
     * <p/>
     * <p>An implementation of this method might remove/destroy a Subject's
     * Principals and Credentials.
     * <p/>
     * <p/>
     *
     * @return true if this method succeeded, or false if this
     *         <code>LoginModule</code> should be ignored.
     * @throws javax.security.auth.login.LoginException
     *          if the logout fails
     */
    public boolean logout()
            throws LoginException
    {
        subject.getPrincipals().clear();
        subject.getPrivateCredentials().clear();
        subject.getPublicCredentials().clear();
        success = false;
        commitSuccess = false;
        try {
            doLogout(subject, params);
        }
        finally {
            params = null;
        }
        return true;
    }

    protected abstract void doLogout(Subject subject, Parameters context)
            throws LoginException;
}
